# -*- coding: utf-8 -*-
# "NotGevent" by Brett Hartshorn : brett@insynchq.com
# Copyright Insynchq Pte. Ltd., ("Insync") | http://www.insynchq.com
# License: BSD
'''
NotGevent - version 0.0.1

This API is a drop in replacement for gevent that is very minimal and designed 
for debugging complex async logic of systems that are built upon gevent.

GreenletExit and Timeout errors are not raised in the worker or spawned threads.
(easy to patch in your own logic here to check for errors)

Greenlets are run in a jobs list by a single worker thread 
(this is for classes that subclass from gevent.Greenlet)

Each gevent.spawn will run in its own thread, returns a _SpawnedThread object
that acts like a gevent.Greenlet.

'''



print('loading NotGevent')
import os, sys, time, threading
import core
import hub

EXTRA_DEBUG = '--extra-debug' in sys.argv

_WORKER_RATE = 0.25   # smaller is more responsive
_GREEN_DELAY = 0.0001 # increase this to debug possible race conditions
_COOPERATIVE_YIELD = 0.01  # not a good idea to have zero sleep? longer delays could expose race conditions.
_MAX_GREEN_JOB_WARNING = 20 # just for debugging, prints a warning when there is this many jobs pending

for arg in sys.argv:
  if arg.startswith( '--worker-rate=' ):
    _WORKER_RATE = float(arg.split('=')[-1])
  elif arg.startswith( '--green-delay=' ):
    _GREEN_DELAY = float(arg.split('=')[-1])
  elif arg.startswith( '--cooperative-yield=' ):
    _COOPERATIVE_YIELD = float(arg.split('=')[-1])
  elif arg.startswith( '--max-jobs-warn=' ):
    _MAX_GREEN_JOB_WARNING = int(arg.split('=')[-1])

# no exceptions are raised - these are fully fake #
class Timeout(object):
  '''Debug where gevent.Timeout breaks logic.
  this is not a BaseException and Python will throw an error anytime we enter
  a function that trys to raise gevent.Timeout
  '''
#class GreenletExit(BaseException): pass
GreenletExit = hub.GreenletExit



def getcurrent(): return hub.MAIN
def joinall( greenlets, timeout=None ):
  for green in greenlets: green.join( timeout=timeout )
def killall( greenlets, timeout=None ):
  for green in greenlets: green.kill()

#################################################################################

def sleep(seconds=_COOPERATIVE_YIELD):
  '''gevent.sleep - Calling sleep with *seconds* of 0 is the canonical way of
    expressing a cooperative yield.'''
  time.sleep( seconds )


def spawn( func, *args, **kw ):
  print('SPAWN',func,args,kw)
  t = _SpawnedThread( func, *args, **kw)
  t.start()
  return t

def spawn_later( later, func, *args, **kw ):
  print('spawn later', later, func)
  t = _SpawnedThread( func, *args, **kw )
  t.delay = later
  t.start()
  return t

## custom 
def force_exit():
  print('force shutdown')
  _greenlet_worker.active = False
  for job in _greenlet_worker._jobs: job.kill() # won't block
  threads = list(_SpawnedThread.THREADS)
  for thread in threads: thread.kill() # won't block
  print('force shutdown done')

class _greenlet_worker(object):
  '''
  run all greenlets in order started
  '''
  active = False
  _jobs = []
  def __init__(self):
    self._thread = threading.Thread( target=self.loop )
    self._thread.daemon = True
    self._thread.start()

  def add( self, greenlet):
    self._jobs.insert( 0, greenlet )
  def loop(self):
    print('\n<<starting simple greenlet worker>>\n')
    self.active = True
    while self.active:
      if len(self._jobs) >= _MAX_GREEN_JOB_WARNING:
        print('WARNING - heavy greenlet load', len(self._jobs))

      if self._jobs:
        greenlet = self._jobs.pop()
        print('\n<<doing new greenjob>> %s \n'%greenlet)
        greenlet._run_fake_greenlet()
        if _GREEN_DELAY: time.sleep( _GREEN_DELAY )
      else:
        time.sleep( _WORKER_RATE )

    print('\n<<stopping simple greenlet worker>>\n')

class NotGreenlet(object):
  WORKER = _greenlet_worker()

  def _run_fake_greenlet(self):

    if not self._killed:
      self.value = self._run( *self._args, **self._kw )

    self.done = True
    ## fake gevent greenlet api - just in case something blocks in here,
    ## a call to kill can at least see that the callback is blocking
    if not self._killed:
      if self._link_value:
        if EXTRA_DEBUG: print('calling greenlet link value', self._link_value)
        self._link_value( self )
      if self._link:
        if EXTRA_DEBUG: print('calling greenlet link', self._link)
        self._link( self )
    self.dead = True

  def kill(self): # block=True, timeout=None
    if self.done and self.dead: return None # normal exit
    elif self.done and not self.dead:
      while not self.dead and _greenlet_worker.active:
        print('waiting for callback to exit', self._link_value, self._link)
        time.sleep(0.1)
    elif not self.done:
      while not self.done and _greenlet_worker.active:
        print('waiting for method to exit', self._run)
        time.sleep(0.1)
      while not self.dead and _greenlet_worker.active:
        print('waiting for callback to exit', self._link_value, self._link)
        time.sleep(0.1)
    elif not self.done and not self.dead:
      print('WARN notgreenlet killed before it was started', self._run)
      self._killed = True
    else:
      raise RuntimeError

  ##################################################

  def _run(self): raise NotImplemented
  def __init__(self, run=None, *args, **kw):
    if EXTRA_DEBUG: print('\nNotGreenlet::', run)
    if args:
      for arg in args:
        if type(arg) is dict:
          for name in arg:
            print(name); print(arg[name])
        else:
          print( args)
    if kw:
      print('-'*80)
      for name in kw:
        print(name); print(kw[name]) 

    if run: self._run = run
    self._args = args
    self._kw = kw
    self._link_value = None
    self._link = None
    self._started = False
    self._killed = False
    self.dead = False
    self.done = False   # greenlet api


  def start(self):
    if EXTRA_DEBUG: print('starting fake greenlet', self._run)
    self._started = True
    self.WORKER.add( self )

  def get(self):
    return self.value
  def link_value(self, callback):
    self._link_value = callback
    if hasattr(self, 'value'): callback(self); self.dead=True

  def link_exception( self, callback):
    print('WARNING not using link-exception',callback)
    self._link_exception = callback

  def join(self, timeout=None):
    if not self._started:
      print('\njoining greenlet\n')
      self.start()
    while not hasattr(self, 'value'):
      print('\ngreenlet waiting...\n')
      time.sleep(0.1)
      if self.dead:
        print('\nWARN - ugly greenlet exit')
        break

  def link(self, callback):
    ''' |      Link greenlet's completion to callable or another greenlet.
 |      
 |      If *receiver* is a callable then it will be called with this instance as an argument
 |      once this greenlet's dead. A callable is called in its own greenlet.
 |      
 |      If *receiver* is a greenlet then an :class:`LinkedExited` exception will be
 |      raised in it once this greenlet's dead.
 |      
 |      If *receiver* is ``None``, link to the current greenlet.
 |      
 |      Always asynchronous, unless receiver is a current greenlet and the result is ready.
 |      If this greenlet is already dead, then notification will performed in this loop
 |      iteration as soon as this greenlet switches to the hub.'''
    self._link = callback
    if hasattr(self, 'value'): callback(self); self.dead=True

Greenlet = NotGreenlet

###########################################################################################


class _SpawnedThread(object):
  THREADS = []
  def __init__(self, func, *args, **kw):
    self.THREADS.append( self )
    self.func = func
    self.args = args
    self.kw = kw
    self.active = False
    self.delay = 0.0
    self.dead = False
    self._callback = None
    self._thread = None

  def join(self, timeout=None):
    if self.dead: print('joining already dead thread')
    elif self._thread:
      print('joining thread', self.func)
      try:
        self._thread.join( timeout=timeout )
      except RuntimeError:
        pass # this can happen if trying to join from the current thread
    else:
      print('ERROR - trying to join thread before it has started', self.func)
      raise RuntimeError

  def start(self):
    print('\n::_SpawnedThread:: %s \n'%self.func)
    #threading._start_new_thread( self.run, tuple() )
    self._thread = threading.Thread( target=self.run )
    self._thread.daemon = True
    self._thread.start()

  def run(self):
    if self.delay: time.sleep(self.delay)
    self.active = True
    self.value = self.func(*self.args, **self.kw)
    if self._callback: self._callback( self ) # greenlet style
    self.active = False
    self.dead = True
    self.THREADS.remove( self )
    print('\n::_SpawnedThread exit:: %s \n'%self.func)

  def kill(self):
    if self.active:
      print('killing _SpawnedThread')
      self.active = False
    self.dead = True

  def link_exception( self, callback):
    print('_SpawnedThread warn not using link-exception',callback)
    self._link_exception = callback

  def link( self, callback ):
    self._callback = callback
    if self.dead:
      print('_SpawnedThread warn - running link callback on dead thread')
      callback( self )


